package com.boot.service.impl;

import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.boot.constants.StatusConstants;
import com.boot.entity.Attachment;
import com.boot.entity.ImageInfo;
import com.boot.entity.Question;
import com.boot.entity.QuestionAssignment;
import com.boot.exception.BusinessException;
import com.boot.mapper.AttachmentMapper;
import com.boot.mapper.ImageInfoMapper;
import com.boot.service.IAttachmentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.boot.service.IQuestionAssignmentService;
import com.boot.service.IQuestionService;
import com.boot.utils.SecurityUtils;
import com.boot.utils.UserContext;
import org.apache.tika.Tika;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.rmi.RemoteException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class AttachmentServiceImpl extends ServiceImpl<AttachmentMapper, Attachment> implements IAttachmentService {

    @Resource
    private IQuestionService questionService;

    @Resource
    private IQuestionAssignmentService questionAssignmentService;

    private final Tika tika = new Tika();

    @Value("${file.uploadDir}")
    private String uploadDirStr;

    @Value("${img.uploadDir}")
    private String imgDirStr;

    @Value("${file.maxSizeKb:5120}") // 默认 5MB
    private int maxSizeKb;

    private Path uploadDir;

    private Path imgDir;

    public static final Set<String> ALLOWED_EXTENSIONS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
            "txt", "log", "out",
            "java", "py", "c", "cpp", "js",
            "html", "htm", "css", "scss", "json", "xml",
            "yml", "yaml", "properties", "sql",
            "md", "markdown"
    )));

    @PostConstruct
    public void init() {
        uploadDir = Paths.get(uploadDirStr);
        if (!Files.exists(uploadDir)) {
            try {
                Files.createDirectories(uploadDir);
            } catch (IOException e) {
                throw new BusinessException("无法创建上传目录: " + uploadDirStr, e);
            }
        }

        imgDir = Paths.get(imgDirStr);
        if (!Files.exists(imgDir)) {
            try {
                Files.createDirectories(imgDir);
            } catch (IOException e) {
                throw new BusinessException("无法创建上传目录: " + imgDirStr, e);
            }
        }
    }

    @Override
    public void validateFile(MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            throw new IllegalArgumentException("文件不能为空");
        }

        long sizeInKb = file.getSize() / 1024;
        if (sizeInKb > maxSizeKb) {
            throw new IllegalArgumentException("文件大小超出限制：" + sizeInKb + "KB，最大允许 " + maxSizeKb + "KB");
        }

        // 清洗原始文件名，只保留最末端的真实文件名
        String fileName = Paths.get(Objects.requireNonNull(file.getOriginalFilename())).getFileName().toString();
        if (fileName.trim().isEmpty()) {
            throw new IllegalArgumentException("文件名不能为空");
        }

        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex < 0 || lastDotIndex == fileName.length() - 1) {
            throw new IllegalArgumentException("文件必须有有效的扩展名");
        }

        String ext = fileName.substring(lastDotIndex + 1).toLowerCase();
        if (!ALLOWED_EXTENSIONS.contains(ext)) {
            throw new IllegalArgumentException("不支持的文件类型: ." + ext);
        }

        // 使用 Tika 检测真实 MIME 类型
        String mimeType;
        try (InputStream is = file.getInputStream()) {
            mimeType = tika.detect(is);
        }
        if (!mimeType.startsWith("text/")) {
            throw new IllegalArgumentException("检测到非法文件类型（非文本）: " + mimeType);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void saveAttachmentsForQuestion(List<MultipartFile> files, Long questionId) throws IOException {
        checkLegalQuestion(questionId);

        // 校验文件名（含扩展名）是否重复
        Set<String> fileNameSet = new HashSet<>();
        for (MultipartFile file : files) {
            String originalFilename = file.getOriginalFilename();
            if (originalFilename == null || originalFilename.trim().isEmpty()) {
                throw new RemoteException("文件名不能为空");
            }

            // 判断是否有重复的文件名（包括扩展名）
            if (!fileNameSet.add(originalFilename)) {
                throw new RemoteException("不允许上传同名文件：" + originalFilename);
            }
        }

        if (files.isEmpty()) {
            return;
        }

        if (files.size() > 10) {
            throw new IllegalArgumentException("一个问题最多只能上传10个附件");
        }

        for (MultipartFile file : files) {
            // 先验证
            validateFile(file);

            // 生成唯一存储名
            String fileName = Paths.get(Objects.requireNonNull(file.getOriginalFilename())).getFileName().toString();
            String ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
            String storedName = IdUtil.fastSimpleUUID() + "." + ext;
            Path targetPath = uploadDir.resolve(storedName);

            // 写入文件
            Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);

            // 构建实体
            Attachment attachment = new Attachment();
            attachment.setQuestionId(questionId);
            attachment.setFileName(fileName);
            attachment.setFileKey(storedName);
            attachment.setMimeType(ext);
            attachment.setSizeKb((int) (file.getSize() / 1024));
            attachment.setUploadedBy(UserContext.getCurrentUserId());
            attachment.setCreatedAt(LocalDateTime.now());

            boolean saved = this.save(attachment);
            if (!saved) {
                throw new IOException("数据库保存失败：" + fileName);
            }
        }
    }

    @Override
    public List<Attachment> listAttachmentsByQuestionId(Long questionId) {
        Long userId = SecurityUtils.requireCurrentUserId();

        QueryWrapper<Question> questionQueryWrapper = new QueryWrapper<>();
        questionQueryWrapper.eq("id", questionId);
        Question question = questionService.getOne(questionQueryWrapper);
        if (question == null) {
            throw new BusinessException("问题不存在");
        }

        // 提问人 或 已接受任务的专家
        if (isNotAssignedOrAcceptedExpert(question, question.getId(), userId)) {
            throw new BusinessException("无权访问");
        }

        QueryWrapper<Attachment> wrapper = new QueryWrapper<>();
        wrapper.eq("question_id", questionId)
                .select("id", "file_name"); // 只查这两个字段
        return this.list(wrapper);
    }

    @Override
    public void streamDownloadAttachmentsAsZip(Long questionId, OutputStream outputStream) throws IOException {
        List<Attachment> attachments = this.list(new QueryWrapper<Attachment>().eq("question_id", questionId));
        if (attachments.isEmpty()) {
            throw new FileNotFoundException("该问题下没有附件");
        }

        try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {

            // 添加每个附件
            for (Attachment att : attachments) {
                Path filePath = uploadDir.resolve(att.getFileKey());
                if (!Files.exists(filePath)) {
                    continue;
                }
                addFileToZip(zos, filePath, "content/" + att.getFileName());
            }

            // 添加安全提醒文件
            String warningText =
                    "⚠️ 安全提醒：此压缩包中的文件由用户上传。\n\n" +
                            "可能包含恶意代码（如 Python 脚本），请勿随意执行！\n\n" +
                            "建议操作：\n" +
                            "- 查看源码内容前，请先审查是否有可疑命令（如 os.system, subprocess, eval 等）\n" +
                            "- 不要直接运行 .py/.sh 等脚本文件\n" +
                            "- 若不确定安全性，请勿打开\n\n" +
                            "平台无法保证所有文件绝对安全，请自行判断风险。\n";

            ZipEntry readmeEntry = new ZipEntry("README_SECURITY.txt");
            zos.putNextEntry(readmeEntry);
            zos.write(warningText.getBytes(StandardCharsets.UTF_8));
            zos.closeEntry();

            zos.finish(); // 必须调用，确保所有数据写出
            // 注意：不要关闭 zos，因为它是包装在 outputStream 上的，真正关闭由外层处理
        }
    }

    private void addFileToZip(ZipOutputStream zos, Path path, String entryName) throws IOException {
        ZipEntry entry = new ZipEntry(entryName);
        zos.putNextEntry(entry);

        try (InputStream is = Files.newInputStream(path)) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = is.read(buffer)) != -1) {
                zos.write(buffer, 0, bytesRead);
            }
        }

        zos.closeEntry();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateAttachmentsForQuestion(List<MultipartFile> files, Long questionId) throws IOException {
        checkLegalQuestion(questionId);

        // 校验文件名（含扩展名）是否重复
        Set<String> fileNameSet = new HashSet<>();
        for (MultipartFile file : files) {
            String originalFilename = file.getOriginalFilename();
            if (originalFilename == null || originalFilename.trim().isEmpty()) {
                throw new BusinessException("文件名不能为空");
            }

            // 判断是否有重复的文件名（包括扩展名）
            if (!fileNameSet.add(originalFilename)) {
                throw new BusinessException("不允许上传同名文件：" + originalFilename);
            }
        }

        if (files.size() > 10) {
            throw new IllegalArgumentException("一个问题最多只能上传10个附件");
        }

        // 查询去除的附件，并进行删除
        List<Attachment> attachments = this.list(new QueryWrapper<Attachment>()
                .eq("question_id", questionId));
        List<String> remainFileNameList = new ArrayList<>();
        for (MultipartFile file : files) {
            if (file.isEmpty()) {
                String fileName = Paths.get(Objects.requireNonNull(file.getOriginalFilename())).getFileName().toString();
                remainFileNameList.add(fileName);
            }
        }
        for (Attachment att : attachments) {
            if (!remainFileNameList.contains(att.getFileName())) {
                Path filePath = uploadDir.resolve(att.getFileKey());
                try {
                    removeById(att.getId());
                    Files.deleteIfExists(filePath);
                } catch (IOException e) {
                    throw new IOException("无法删除文件：" + att.getFileKey(), e);
                }
            }
        }

        for (MultipartFile file : files) {
            if (file.isEmpty()) {
                continue;
            }

            // 先验证
            validateFile(file);

            // 生成唯一存储名
            String fileName = Paths.get(Objects.requireNonNull(file.getOriginalFilename())).getFileName().toString();
            String ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
            String storedName = IdUtil.fastSimpleUUID() + "." + ext;
            Path targetPath = uploadDir.resolve(storedName);

            // 写入文件
            Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);

            // 构建实体
            Attachment attachment = new Attachment();
            attachment.setQuestionId(questionId);
            attachment.setFileName(fileName);
            attachment.setFileKey(storedName);
            attachment.setMimeType(ext);
            attachment.setSizeKb((int) (file.getSize() / 1024));
            attachment.setUploadedBy(UserContext.getCurrentUserId());
            attachment.setCreatedAt(LocalDateTime.now());

            boolean saved = this.save(attachment);
            if (!saved) {
                throw new IOException("数据库保存失败：" + fileName);
            }
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteAttachmentsForQuestion(Long questionId) throws IOException {
        // 查询这些附件是否属于该问题
        List<Attachment> attachments = this.list(new QueryWrapper<Attachment>()
                .eq("question_id", questionId));

        // 删除文件系统中的文件
        for (Attachment att : attachments) {
            Path filePath = uploadDir.resolve(att.getFileKey());
            try {
                // 删除数据库记录
                this.removeById(att);
                Files.deleteIfExists(filePath);
            } catch (IOException e) {
                throw new IOException("无法删除文件：" + att.getFileKey(), e);
            }
        }
    }

    private void checkLegalQuestion(Long questionId) {
        Long userId = SecurityUtils.requireCurrentUserId();

        Question question = questionService.getById(questionId);
        if (question == null) {
            throw new BusinessException("问题不存在");
        }

        if (!question.getUserId().equals(userId)) {
            throw new BusinessException("无权操作他人问题");
        }
    }

    @Override
    public boolean isNotAssignedOrAcceptedExpert(Question question, Long questionId, Long userId) {
        // 权限检查：必须是提问者本人或处理专家
        if (!question.getUserId().equals(userId)) {
            // 检查是否为被分配且已接受的专家
            QueryWrapper<QuestionAssignment> wrapper = new QueryWrapper<>();
            wrapper.eq("question_id", questionId)
                    .eq("expert_id", userId)
                    .and(w -> w
                            .isNull("accepted")           // 待响应
                            .or()
                            .eq("accepted", StatusConstants.AssignmentStatus.ACCEPTED)   // 或已接受
                    );
            return questionAssignmentService.count(wrapper) <= 0;
        }
        return false;
    }

    @Value("${img.prefix}")
    private String imgPrefix;

    @Resource
    private ImageInfoMapper imageInfoMapper;

    @Override
    public String uploadImage(MultipartFile file) throws Exception {
        // 1. 文件非空校验
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("上传的文件不能为空");
        }

        // 2. 检查文件大小（比如限制 5MB）
        long maxSize = 5 * 1024 * 1024; // 5MB
        if (file.getSize() > maxSize) {
            throw new IllegalArgumentException("图片大小不能超过 5MB");
        }

        // 3. 校验是否是图片格式
        BufferedImage image;
        try {
            image = ImageIO.read(file.getInputStream());
            if (image == null) {
                throw new IllegalArgumentException("仅支持有效的图片文件（PNG, JPG, JPEG, GIF, WebP）");
            }
        } catch (IOException e) {
            throw new IllegalArgumentException("无法读取图片文件，请检查格式");
        }

        // 4. 获取原始文件扩展名
        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null) {
            throw new IllegalArgumentException("文件名无效");
        }
        String extension = originalFilename.substring(originalFilename.lastIndexOf('.'));

        // 5. 构建存储路径（按日期分目录）
        String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        Path targetDir = imgDir.resolve(datePath);
        Files.createDirectories(targetDir);

        // 6. 生成唯一文件名：UUID作为存储路径中的文件名
        String storageFileName = UUID.randomUUID() + extension;
        Path targetPath = targetDir.resolve(storageFileName);

        // 7. 保存文件到磁盘
        Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);

        // 8. 生成对外公开的 imageName：年月日时分秒毫秒 + UUID + 后缀
        String imageName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))
                + "_" + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8)
                + extension;

        // 9. 写入数据库记录
        ImageInfo imageInfo = new ImageInfo();
        imageInfo.setImageName(imageName);
        imageInfo.setStoragePath(datePath + "/" + storageFileName); // 存相对路径即可
        imageInfo.setCreatedAt(LocalDateTime.now());
        imageInfo.setStatus(StatusConstants.ImageStatus.APPROVED);

        Long userId = SecurityUtils.requireCurrentUserId();
        imageInfo.setUserId(userId);

        boolean saved = imageInfoMapper.insert(imageInfo) > 0;
        if (!saved) {
            // 若插入失败，删除已存文件
            try {
                Files.deleteIfExists(targetPath);
            } catch (IOException ignored) {}
            throw new IOException("图片信息保存失败");
        }

        // 10. 返回可访问 URL（通过 imageName 访问）
        return imgPrefix + imageName;
    }

    @Override
    public ResponseEntity<StreamingResponseBody> serveImage(String imageName) throws Exception {
        if (imageName == null || imageName.trim().isEmpty()) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }

        // 解码防止中文乱码
        String decodedImageName = URLDecoder.decode(imageName, "UTF-8");

        // 查询数据库获取真实存储路径
        ImageInfo imageInfo = imageInfoMapper.selectOne(
                new QueryWrapper<ImageInfo>().eq("image_name", decodedImageName)
        );

        if (imageInfo == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .contentType(MediaType.TEXT_PLAIN)
                    .body(out -> out.write("图片不存在".getBytes(StandardCharsets.UTF_8)));
        }

        if (!StatusConstants.ImageStatus.APPROVED.equals(imageInfo.getStatus())) {
            // 构造一个表示“审核中”的 SVG 图像响应
            String svgContent = generatePendingSvg();

            // 转换为字节数组
            byte[] svgBytes = svgContent.getBytes(StandardCharsets.UTF_8);

            HttpHeaders headers = new HttpHeaders();
            headers.setContentLength(svgBytes.length);

            StreamingResponseBody body = outputStream -> outputStream.write(svgBytes);

            return ResponseEntity.ok()
                    .headers(headers)
                    .body(body);
        }

        // 组合完整物理路径
        Path filePath = imgDir.resolve(imageInfo.getStoragePath());
        Path normalized = filePath.normalize();

        // 安全检查：防止路径穿越
        if (!normalized.startsWith(imgDir.normalize())) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null);
        }

        if (!Files.exists(normalized)) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .contentType(MediaType.TEXT_PLAIN)
                    .body(out -> out.write("图片文件丢失".getBytes(StandardCharsets.UTF_8)));
        }

        if (!Files.isRegularFile(normalized)) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
        }

        // 推断 Content-Type
        String contentType = Files.probeContentType(normalized);
        if (contentType == null || !contentType.startsWith("image/")) {
            contentType = "image/octet-stream";
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType(contentType));
        headers.setContentLength(Files.size(normalized));

        StreamingResponseBody body = outputStream -> {
            byte[] buffer = new byte[8192];
            int bytesRead;
            try (InputStream in = Files.newInputStream(normalized)) {
                while ((bytesRead = in.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
                outputStream.flush();
            }
        };

        return ResponseEntity.ok()
                .headers(headers)
                .body(body);
    }

    private String generatePendingSvg() {
        return "<svg width='400' height='200' xmlns='http://www.w3.org/2000/svg'>\n" +
                "  <!-- Background -->\n" +
                "  <rect width='100%' height='100%' fill='#fff3cd'/>\n" +
                "  <!-- Icon: Clock or Warning -->\n" +
                "  <circle cx='80' cy='100' r='40' fill='#ffc107'/>\n" +
                "  <path d='M80 65 v30 l20 20' stroke='black' stroke-width='5' fill='none'/>\n" +
                "  <!-- Text -->\n" +
                "  <text x='130' y='90' font-family='sans-serif' font-size='28' fill='#856404'>禁用</text>\n" +
                "  <text x='130' y='130' font-family='sans-serif' font-size='16' fill='#856404'>图片已被禁用</text>\n" +
                "</svg>";
    }
}
